DELETE Product Event Endpoint API Documentation

Overview

Endpoint: DELETE /product-event/:id

Description: Soft delete an Event and all associated data including related Products and Posts.

Authentication: JWT Bearer Token (Global JwtAuthGuard)


Request Flow

1. Controller Layer (src/product-event/product.event.controller.ts)

Location: Lines 76-81

@Delete(':id')
async delete(@Param('id') id: string) {
  const userId = this.requestContextService.getUserId();
  await this.productEventService.delete(id, userId);
  return 'ok';
}

Key Points:

  • Route Parameter: id - Event UUID
  • Authentication: Uses global JwtAuthGuard (no explicit @UseGuards decorator)
  • User Context: Gets userId from RequestContextService
  • Response: Returns string 'ok' on success

2. Service Layer (src/product-event/product.event.service.ts)

Location: Lines 1169-1187

async delete(id: string, curatorId: string) {
  // Step 1: Verify event ownership
  const entity = await this.findUniqueEventEntityOrThrow(id, curatorId);

  // Step 2: Check for existing orders
  const hasOrders = await this.repo.hasExistingOrders([id]);
  if (hasOrders) {
    throw new BadRequestException(
      "This event can't be deleted because it already has customer orders.",
    );
  }

  // Step 3: Get related product IDs
  const productIds = entity.relatedProductIds();

  // Step 4: Soft delete event and products
  await this.repo.delete([id], productIds);

  // Step 5: Delete related posts
  await this.syncDeletePosts(entity);

  // Step 6: Queue async processing
  await this.productEventPublisher.onDelete(productIds);
}

Step 2a: Ownership Verification

Method: findUniqueEventEntityOrThrow() Location: src/product-event-core/product-event-core.service.ts (Lines 93-109)

async findUniqueEventEntityOrThrow(id: string, curatorId?: string): Promise<EventEntity> {
  const result = await this.findUniqueEventEntity(id, curatorId);
  if (!result) {
    throw new ResourceNotFound({ resource: 'Event', id });
  }
  return result;
}

Query: Filters by both id AND curatorId to ensure ownership

Step 2b: Order Existence Check

Method: hasExistingOrders() Location: src/product-event/product.event.repo.ts (Lines 652-663)

async hasExistingOrders(eventIds: string[]) {
  const orderLineItems = await this.prisma.orderLineItem.findMany({
    where: {
      unitPrice: { gt: 0 },  // Only paid orders count
      eventId: { in: eventIds },
    },
    take: 1,
  });
  return orderLineItems.length > 0;
}

Business Rule: Events with paid orders (unitPrice > 0) cannot be deleted.

Method: EventEntity.relatedProductIds() Location: src/product-event/entity/product.event.entity.ts (Lines 238-240)

relatedProductIds(): string[] {
  return this.relatedProduct()?.map((p) => p.id) ?? [];
}

Returns all Product IDs associated with the Event (ticket products, event products).


3. Repository Layer - Soft Delete (src/product-event/product.event.repo.ts)

Location: Lines 477-497

async delete(ids: string[], productIds: string[]) {
  await this.prisma.$transaction(async (transaction) => {
    // Soft delete Event(s)
    await transaction.event.updateMany({
      where: { id: { in: ids } },
      data: { deletedAt: new Date() },
    });

    // Soft delete related Product(s)
    isEmpty(productIds)
      ? null
      : await transaction.product.updateMany({
          where: { id: { in: productIds } },
          data: {
            deletedAt: new Date(),
            isAvailable: false,
          },
        });
  });
}

Transaction Scope: All database operations are atomic (all succeed or all fail).

Database Operations:

  1. Event.deletedAt = NOW() - Soft delete event
  2. Product.deletedAt = NOW() - Soft delete products
  3. Product.isAvailable = false - Mark products unavailable

4. Post Deletion (src/product-event/product.event.service.ts)

Method: syncDeletePosts() Location: Lines 1190-1203

private async syncDeletePosts(entity: EventEntity) {
  const needDeletePostIds = (await this.repo.findPostsByEventId(entity)).map(
    (post) => post.id,
  );

  await Promise.all([
    ...needDeletePostIds.map(async (postId) => {
      return await this.postsCuratorService.deletePost(
        entity.curatorId,
        postId,
        true,
      );
    }),
  ]);
}

Target Posts: All Posts where post.createFromEventId === event.id

Post Deletion Service (src/posts/curator/posts.curator.service.ts, Lines 1955-1971):

async deletePost(userId: string, postId: string, isChildPostExpired: boolean = false) {
  const post = await this.postsService.findPostById(postId);

  if (!post || post.deletedAt || post.creatorId !== userId) {
    throw new ResourceNotFound({ postId });
  }

  const data = await this.repo.deletePost(postId, isChildPostExpired);
  await this.postsPublisher.removePostCoupons(data.childPostIds.concat(postId));
  return data.post;
}

Post Deletion Flow:

  1. Verify post ownership (creatorId === userId)
  2. Soft delete post (deletedAt = NOW())
  3. Remove associated coupons

5. Queue Processing

Publisher (src/product-event/event/product.event.publisher.ts)

Location: Lines 65-72

async onDelete(productIds: string[]) {
  if (isEmpty(productIds)) {
    return;
  }
  await this.productEventQueue.add('onDelete', { productIds });
}

Queue: Event queue (Bull) Job Name: onDelete Job Data: { productIds: string[] }

Subscriber (src/product-event/event/product.event.subscriber.ts)

Location: Lines 34-47

@Process('onDelete')
async onDelete(job: Job<{ productIds: string[] }>) {
  const { productIds } = job.data;
  const products =
    await this.merchantProductsRepository.findManyFromDbOnly(productIds);

  await Promise.all(
    products.map(async (product) => {
      await this.merchantProductsService.afterProductDeleted(product);
    }),
  );
}

Processing: For each deleted Product, call afterProductDeleted()

Post-Delete Handler (src/products/merchant-products/merchant-products.service.ts)

Location: Lines 488-497

async afterProductDeleted(data: ProductComplete | null) {
  if (data) {
    await this.busService.emit(
      EventNames.UpdateMerchantProduct,
      data,
      'delete',
    );
  }
  return data;
}

Event Emitted: UpdateMerchantProduct with operation type 'delete'


Complete Flow Diagram

┌──────────────────────────────────────────────────────────────────────┐
│ DELETE /product-event/:id                                            │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐    │
│  │ 1. Controller Layer                                          │    │
│  │    - Extract userId from RequestContextService              │    │
│  │    - Call productEventService.delete(id, userId)           │    │
│  └───────────────────────┬─────────────────────────────────────┘    │
│                          │                                           │
│  ┌───────────────────────▼─────────────────────────────────────┐    │
│  │ 2. Service Layer                                            │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ 2a. Verify Event Ownership                          │  │    │
│  │    │     - findUniqueEventEntityOrThrow(id, curatorId)   │  │    │
│  │    │     - Throws ResourceNotFound if not owner          │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ 2b. Check for Existing Orders                      │  │    │
│  │    │     - hasExistingOrders([id])                       │  │    │
│  │    │     - Throws BadRequestException if hasPaidOrders   │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ 2c. Get Related Product IDs                        │  │    │
│  │    │     - entity.relatedProductIds()                    │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ 2d. Soft Delete (Database Transaction)             │  │    │
│  │    │     - repo.delete([id], productIds)                 │  │    │
│  │    │     - Event.deletedAt = NOW()                       │  │    │
│  │    │     - Product.deletedAt = NOW()                      │  │    │
│  │    │     - Product.isAvailable = false                    │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ 2e. Delete Related Posts                           │  │    │
│  │    │     - syncDeletePosts(entity)                       │  │    │
│  │    │     - Find posts with createFromEventId === event.id│  │    │
│  │    │     - Call postsCuratorService.deletePost()         │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ 2f. Queue Async Processing                          │  │    │
│  │    │     - productEventPublisher.onDelete(productIds)     │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  └───────────────────────┬─────────────────────────────────────┘    │
│                          │                                           │
│  ┌───────────────────────▼─────────────────────────────────────┐    │
│  │ 3. Queue Processing (Async)                                 │    │
│  │    ┌─────────────────────────────────────────────────────┐  │    │
│  │    │ Subscriber.onDelete()                               │  │    │
│  │    │ - For each product: afterProductDeleted(product)    │  │    │
│  │    │ - Emit UpdateMerchantProduct event (delete)         │  │    │
│  │    └─────────────────────────────────────────────────────┘  │    │
│  └──────────────────────────────────────────────────────────────┘    │
│                                                                      │
│  Response: 'ok'                                                      │
└──────────────────────────────────────────────────────────────────────┘

Business Rules

1. Ownership Requirement

  • Rule: Only the Event owner (curatorId) can delete the Event
  • Enforcement: Database query filters by both id and curatorId
  • Error: ResourceNotFound if Event doesn't exist or user doesn't own it

2. Existing Orders Protection

  • Rule: Events with paid orders (unitPrice > 0) cannot be deleted
  • Reason: Prevents data integrity issues with historical order data
  • Error: BadRequestException with message "This event can't be deleted because it already has customer orders."
  • Note: Free orders (unitPrice = 0) do not prevent deletion

3. Soft Delete Pattern

  • Event: deletedAt timestamp set (not hard deleted from database)
  • Products: Both deletedAt set AND isAvailable = false
  • Reason: Preserves historical data while hiding from UI

4. Cascading Deletions

  • Products: All related products (tickets, event products) soft deleted
  • Posts: All Posts with createFromEventId === event.id soft deleted
  • Coupons: Associated coupons removed via Post deletion flow

5. Transaction Safety

  • All database operations wrapped in Prisma transaction
  • Either ALL succeed or ALL fail (atomic operation)

Request/Response Examples

Request

curl 'https://release.katana-api.1m.app/product-event/b4e38b06-7902-4ba6-ad7e-5429417927d3' \
  -X 'DELETE' \
  -H 'authorization: Bearer <JWT_TOKEN>' \
  -H 'from: client'

Success Response

HTTP/1.1 200 OK
Content-Type: application/json

"ok"

Error Responses

Event Not Found / Not Owned:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "statusCode": 404,
  "message": "Resource Event with id b4e38b06-7902-4ba6-ad7e-5429417927d3 not found"
}

Existing Orders:

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "statusCode": 400,
  "message": "This event can't be deleted because it already has customer orders."
}

  • GET /product-event/:id/has-existing-orders - Check if event has orders before attempting deletion
  • GET /product-event/:id/details - Get event details
  • GET /product-event/list - List user's events

Database Schema Affected

Event Table

UPDATE "Event" SET "deletedAt" = NOW() WHERE "id" = $1;

Product Table

UPDATE "Product"
SET "deletedAt" = NOW(), "isAvailable" = false
WHERE "id" = ANY($1);

Post Table

UPDATE "Post" SET "deletedAt" = NOW()
WHERE "createFromEventId" = $1;

Queue Configuration

Queue Name: Event Job Options (from src/product-event/product.event.module.ts):

{
  attempts: 3,
  removeOnComplete: 100,
  removeOnFail: 100,
  backoff: {
    type: 'exponential',
    delay: 1000,
  },
}

Rate Limiting: limiter: { max: 100, duration: 100 } (100 jobs per 100ms)


Testing Considerations

Unit Tests Needed

  1. Ownership Verification: Non-owner cannot delete event
  2. Order Protection: Event with orders cannot be deleted
  3. Soft Delete: Verify deletedAt timestamps set correctly
  4. Product Availability: Verify isAvailable = false on products
  5. Post Deletion: Verify related posts deleted
  6. Queue Processing: Verify onDelete event published

Integration Tests Needed

  1. Full Flow: Delete event → verify all cascading deletions
  2. Transaction Rollback: Verify rollback on partial failure
  3. Queue Processing: Verify async completion
  4. Cross-Module: Verify Post service integration

Monitoring & Logging

Key Logging Points

  1. Service Entry: Log delete request with eventId and userId
  2. Order Check: Log when deletion blocked due to existing orders
  3. Repository: Log transaction success/failure
  4. Post Deletion: Log number of posts deleted
  5. Queue: Log job added to queue

Error Monitoring

  • Track BadRequestException for order protection (may indicate UX issues)
  • Track ResourceNotFound for ownership failures (potential security concerns)
  • Monitor queue job failures

References

  • Controller: src/product-event/product.event.controller.ts:76-81
  • Service: src/product-event/product.event.service.ts:1169-1187
  • Repository: src/product-event/product.event.repo.ts:477-497
  • Publisher: src/product-event/event/product.event.publisher.ts:65-72
  • Subscriber: src/product-event/event/product.event.subscriber.ts:34-47
  • Post Service: src/posts/curator/posts.curator.service.ts:1955-1971

Last Updated: 2026-02-25 Document Version: 1.0

results matching ""

    No results matching ""